package model;


import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logProfilerBegin;
import static com.nightpiter.controller.log.Logger.logProfilerEnd;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import model.objects.EdiLine;
import model.objects.EdiStation;
import model.objects.EdiTransfer;
import model.objects.EdiTunnel;

import com.nightpiter.model.exceptions.BindingBuilderException;
import com.nightpiter.model.exceptions.LineBindingBuilderException;
import com.nightpiter.model.exceptions.ModelNotAssignedBuilderException;
import com.nightpiter.model.exceptions.StationBindingBuilderException;
import com.nightpiter.model.objects.Coordinates;
import com.nightpiter.model.objects.Hub;
import com.nightpiter.model.objects.Line;
import com.nightpiter.model.objects.Station;
import com.nightpiter.model.objects.Time;
import com.nightpiter.model.objects.Transfer;
import com.nightpiter.model.objects.Tunnel;


/**
 * MetroModelBuider; realization of Builder pattern
 * 
 * Used to construct such complex object as MetroModel instance; performs
 * bind-by-id resolving between objects of Station, Line, Tunnel and Transfer
 * classes
 * 
 * suitable for SAX-, and for DOM-reader; for DOM use DomBuilderWrapper
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroModelBuilder {
	/**
	 * temporary structure class to store raw data about Station element read
	 * from the XML
	 */
	private class StationStruct {
		public int id = 0;
		public Time open, close;
		public String name;
		public int labelX, labelY;
		public int labelAngle;
		public boolean labelVisible;
		public Coordinates position;
		public Coordinates location;
	}

	/**
	 * temporary structure class to store raw data about Tunnel element read
	 * from the XML
	 */
	private class TunnelStruct {
		public int from, to, line;
		public int time;
		public boolean smooth;
	}

	/**
	 * temporary structure class to store raw data about Transfer element read
	 * from the XML
	 */
	private class TransferStruct {
		public int from, to;
		public int time;
	}

	/**
	 * temporary structure class to store raw data about Line element read from
	 * the XML
	 */
	private class LineStruct {
		public int id = 0, color = 0xff777777;
		public String name;
	}

	/**
	 * this field stores a model we have to construct
	 * 
	 * it's null in the initial state
	 */
	private MetroModel model;

	/**
	 * this field contains data that we will pass to the model's constructor
	 */
	private Hashtable<Integer, EdiStation> stationHash = new Hashtable<Integer, EdiStation>();

	/**
	 * this field is needed when we construct Tunnels
	 */
	private Hashtable<Integer, EdiLine> lineHash = new Hashtable<Integer, EdiLine>();

	/**
	 * temporary structure to write station data in
	 */
	private StationStruct stationStruct = new StationStruct();

	/**
	 * temporary structure to write line data in
	 */
	private LineStruct lineStruct = new LineStruct();

	/**
	 * Collection of Tunnel temporary structures
	 */
	private ArrayList<TunnelStruct> tunnelStructCollection = new ArrayList<TunnelStruct>();

	/**
	 * Collection of Transfer temporary structures
	 */
	private ArrayList<TransferStruct> transferStructCollection = new ArrayList<TransferStruct>();

	/**
	 * the name of the city represented in the Model
	 */
	private String cityName = "";

	/**
	 * comment to the map
	 */
	private String comment = "";

	/**
	 * authors' names
	 */
	private ArrayList<String> authors = new ArrayList<String>();

	// TODO this should be specified in xml for each line element
	private final static int DEFAULT_TRAIN_DELAY = 100;

	/**
	 * public constructor
	 * 
	 * @param model
	 *            the MetroModel to build
	 * @throws ModelNotAssignedBuilderException
	 */
	public MetroModelBuilder(MetroModel model)
			throws ModelNotAssignedBuilderException {
		if (model == null) {
			throw new ModelNotAssignedBuilderException();
		}
		this.model = model;		
	}

	/**
	 * performs bind-by-id resolving and passes the result to the model's build
	 * method
	 * 
	 * @throws StationBindingBuilderException
	 * @throws LineBindingBuilderException
	 */
	public void buildModel() throws BindingBuilderException {
		logProfilerBegin("building the Model");
		
		log("creating tunnels");
		ArrayList<EdiTunnel> tunnels = new ArrayList<EdiTunnel>(
				tunnelStructCollection.size());
		
		for (TunnelStruct tunnelStruct : tunnelStructCollection) {
			Station fromStation = stationHash.get(tunnelStruct.from);
			if (fromStation == null) {
				throw new StationBindingBuilderException(tunnelStruct.from,
						"from", "tunnel");
			}
			Station toStation = stationHash.get(tunnelStruct.to);
			if (toStation == null) {
				throw new StationBindingBuilderException(tunnelStruct.to, "to",
						"tunnel");
			}
			Line line = lineHash.get(tunnelStruct.line);
			if (line == null) {
				throw new LineBindingBuilderException(tunnelStruct.line,
						"line", "tunnel");
			}
			tunnels.add(new EdiTunnel(fromStation, toStation, tunnelStruct.time,
					line, tunnelStruct.smooth));
		}
		
		log("creating transfers");
		ArrayList<EdiTransfer> transfers = new ArrayList<EdiTransfer>(
				transferStructCollection.size());
		
		for (TransferStruct transferStruct : transferStructCollection) {
			Station fromStation = stationHash.get(transferStruct.from);
			if (fromStation == null) {
				throw new StationBindingBuilderException(transferStruct.from,
						"from", "transfer");
			}
			Station toStation = stationHash.get(transferStruct.to);
			if (toStation == null) {
				throw new StationBindingBuilderException(transferStruct.to,
						"to", "transfer");
			}
			transfers.add(new EdiTransfer(fromStation, toStation,
					transferStruct.time));
		}
		

		tunnelStructCollection = null;
		transferStructCollection = null;

		log("preparing stations");
		ArrayList<EdiStation> stations = new ArrayList<EdiStation>(stationHash
				.values());
		log("preparing lines");
		ArrayList<EdiLine> lines = new ArrayList<EdiLine>(lineHash.values());

		log("creating hubs");
		
		ArrayList<Hub> hubs = processHubs(transfers);
		
		this.model.build(new EditorMetroModelData(stations, transfers, tunnels, hubs,
				lines, cityName, authors, comment));
		logProfilerEnd("building the Model");
	}
	public static ArrayList<Hub> processHubs(List<EdiTransfer> transfers) {
		ArrayList<Hub> hubs = new ArrayList<Hub>();
		for (EdiTransfer transfer : transfers) {			
			transfer.resetHub();
			if (transfer.getHub() == null) {
				Hub hub = searchHub(transfer);
				if (!hubs.contains(hub)) {
					hubs.add(hub);
				}
			}
		}
		return hubs;
	}
	/**
	 * searches all neighbor transfers graph and creates new hub if necessary;
	 * there are some analogyes with Dijkstra's algorithm but this one is
	 * simpler
	 * 
	 * @param transfer
	 * @return new hub created
	 */
	private static Hub searchHub(Transfer transfer) {
		ArrayList<Transfer> neighbors = new ArrayList<Transfer>();
		ArrayList<Transfer> visiteds = new ArrayList<Transfer>();
		neighbors.add(transfer);
		visiteds.add(transfer);

		Hub hub = null;
		// TODO this algorithm is too ugly, may be think about another way?
		while (!neighbors.isEmpty()) {
			Transfer neighborTransfer = neighbors.remove(0);
			ArrayList<Transfer> fromStationTransfers = neighborTransfer
					.getFromStation().getTransfers();
			for (Transfer transferToAdd : fromStationTransfers) {
				hub = transferToAdd.getFromStation().getHub();
				if (hub != null) {
					break;
				}
				hub = transferToAdd.getToStation().getHub();
				if (hub != null) {
					break;
				}
				if (!visiteds.contains(transferToAdd)) {
					neighbors.add(transferToAdd);
					visiteds.add(transferToAdd);
				}
			}
			if (hub != null) {
				neighbors.clear();
				break;
			}
			ArrayList<Transfer> toStationTransfers = neighborTransfer
					.getToStation().getTransfers();
			for (Transfer transferToAdd : toStationTransfers) {
				hub = transferToAdd.getFromStation().getHub();
				if (hub != null) {
					break;
				}
				hub = transferToAdd.getToStation().getHub();
				if (hub != null) {
					break;
				}
				if (!visiteds.contains(transferToAdd)) {
					neighbors.add(transferToAdd);
					visiteds.add(transferToAdd);
				}
			}
			if (hub != null) {
				neighbors.clear();
				break;
			}
		}
		if (hub == null) {
			hub = new Hub();
		}

		neighbors.add(transfer);
		hub.addTransfer(transfer);
		while (!neighbors.isEmpty()) {
			Transfer neighborTransfer = neighbors.remove(0);
			ArrayList<Transfer> fromStationTransfers = neighborTransfer
					.getFromStation().getTransfers();

			for (Transfer transferToAdd : fromStationTransfers) {
				if (transferToAdd.getHub() == null) {
					neighbors.add(transferToAdd);
					hub.addTransfer(transferToAdd);
				}
			}

		}

		return hub;
	}

	/**
	 * called when the city's name retrieved
	 * 
	 * @param cityName
	 */
	public void setCityName(String cityName) {
		this.cityName = cityName;
	}

	/**
	 * called when the author name retrieved
	 * 
	 * @param authorName
	 */
	public void setAuthorName(String authorName) {
		this.authors.add(authorName);
	}

	public void setNote(String note) {
		this.comment = note;
	}

	/**
	 * called when a new station element has occured and it's id's retrieved
	 * 
	 * @param id
	 */
	public void startStation(int id) {
		this.stationStruct.id = id;
		this.stationStruct.position = null;
		this.stationStruct.location = null;
		this.stationStruct.open = null;
		this.stationStruct.close = null;
		this.stationStruct.name = null;
	}

	/**
	 * sets the new station's name
	 * 
	 * @param name
	 */
	public void setStationName(String name) {
		this.stationStruct.name = name;
	}

	/**
	 * sets the new station's label properties
	 * 
	 * @param x
	 * @param y
	 */
	public void setStationNameAttrs(int dx, int dy, int angle, boolean visible) {
		this.stationStruct.labelX = dx;
		this.stationStruct.labelY = dy;
		this.stationStruct.labelAngle = angle;
		this.stationStruct.labelVisible = visible;
	}

	/**
	 * sets the new station's position
	 * 
	 * @param x
	 * @param y
	 */
	public void setStationPosition(int x, int y) {
		this.stationStruct.position = new Coordinates(x, y);
	}

	/**
	 * sets the new station's geo location
	 * 
	 * @param latitude
	 * @param longitude
	 */
	public void setStationLocation(int latitude, int longitude) {
		this.stationStruct.location = new Coordinates(latitude, longitude);
	}

	/**
	 * sets the new station's open and close schedule
	 * 
	 * @param open
	 * @param close
	 */
	public void setStationSchedule(Time open, Time close) {
		this.stationStruct.open = open;
		this.stationStruct.close = close;
	}

	/**
	 * instantiates Station with retrieved parameters
	 */
	public void endStation() {
		if (this.stationStruct.name == null) {
			this.stationStruct.name = "";
		}
		if (this.stationStruct.position == null) {
			this.stationStruct.position = new Coordinates(0, 0);
		}
		if (this.stationStruct.location == null) {
			this.stationStruct.location = new Coordinates(0, 0);
		}
		if (this.stationStruct.open == null) {
			this.stationStruct.open = new Time(0, 0, 0);
		}
		if (this.stationStruct.close == null) {
			this.stationStruct.close = new Time(0, 0, 0);
		}
		stationHash.put(stationStruct.id, new EdiStation(stationStruct.name,
				stationStruct.position, stationStruct.location,
				stationStruct.open, stationStruct.close, stationStruct.labelX,
				stationStruct.labelY, stationStruct.labelAngle,
				stationStruct.labelVisible));
	}

	/**
	 * add's a new Tunnel to future model
	 * 
	 * @param id
	 * @param to
	 * @param from
	 * @param time
	 * @param line
	 * @param smooth
	 */
	public void addTunnel(int to, int from, int time, int line, boolean smooth) {
		TunnelStruct tunnelStruct = new TunnelStruct();
		tunnelStructCollection.add(tunnelStruct);
		tunnelStruct.to = to;
		tunnelStruct.from = from;
		tunnelStruct.line = line;
		tunnelStruct.time = time;
		tunnelStruct.smooth = smooth;
	}

	/**
	 * add's a new Transfer to future model
	 * 
	 * @param id
	 * @param to
	 * @param from
	 * @param time
	 */
	public void addTransfer(int to, int from, int time) {
		TransferStruct transferStruct = new TransferStruct();
		transferStructCollection.add(transferStruct);
		transferStruct.to = to;
		transferStruct.from = from;
		transferStruct.time = time;
	}

	/**
	 * called when a new line element has occured and it's id's retrieved
	 * 
	 * @param id
	 */
	public void startLine(int id) {
		this.lineStruct.id = id;
	}

	/**
	 * sets the new line's name
	 * 
	 * @param name
	 */
	public void setLineName(String name) {
		this.lineStruct.name = name;
	}

	/**
	 * sets the new line's color
	 * 
	 * @param color
	 */
	public void setLineColor(int color) {
		this.lineStruct.color = color;
	}

	/**
	 * instantiates Line with retrieved parameters
	 */
	public void endLine() {
		lineHash.put(lineStruct.id, new EdiLine(lineStruct.name, lineStruct.color,
				DEFAULT_TRAIN_DELAY));
	}

	/**
	 * called when entering the root element
	 * 
	 */
	public void startMap() {
		
	}

	/**
	 * called when exiting the root element
	 */
	public void endMap() {
		
	}

	/**
	 * called when entering lines block
	 */
	public void startLines() {
		
	}

	/**
	 * called when exiting lines block
	 */
	public void endLines() {
		
	}

	/**
	 * called when entering stations block
	 */
	public void startStations() {
		
	}

	/**
	 * called when exiting stations block
	 */
	public void endStations() {
		
	}

	/**
	 * called when entering tunnels block
	 */
	public void startTunnels() {
		
	}

	/**
	 * called when exiting tunnels block
	 */
	public void endTunnels() {
		
	}

	/**
	 * called when entering transfers block
	 */
	public void startTransfers() {
		
	}

	/**
	 * called when exiting transfers block
	 */
	public void endTransfers() {
		
	}

}